﻿/*
 * Copyright 2011 Edward L. Stauff.                           Contact: <EdStauff@gmail.com>.
 * 
 * This file, "XerialMemberMappings.cs", is part of the "Stauffware.Common.Xml.Xerialization" 
 * library.  "Stauffware.Common.Xml.Xerialization" is free software: you can redistribute 
 * it and/or modify it under the terms of the GNU Lesser Public License as published by 
 * the Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common.Xml.Xerialization" is distributed in the hope that it will be 
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common.Xml.Xerialization".  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Xml;
using System.Text;
using System.Reflection;
using Stauffware.Common.Core;

namespace Stauffware.Common.Xml.Xerialization
{
	//#################################################################################
	/// <summary>
	/// Base class for mapping class members
	/// </summary>

	public abstract class XerialMemberMapping : XerialMapping
	{
		/// <summary> Is the mapped class member optional? </summary>
		public readonly bool IsOptional;

		/// <summary> For getting and setting values. </summary>
		protected readonly MemberHelper Helper;

		/// <inherit/>
		protected override XerialContext Context { get { return this._context; } }
		private readonly XerialContext _context;

		/// <summary> constructor </summary>
		/// <param name="context">the XerialContext to which this mapping belongs</param>
		/// <param name="xmlName">the XML name of the mapped member</param>
		/// <param name="helper">for reading and writing values</param>
		/// <param name="isOptional">specifies whether the mapped member is optional</param>
		protected XerialMemberMapping (XerialContext context, string xmlName, 
										MemberHelper helper, bool isOptional)
			: base(xmlName)
		{
			Require.NotNull(context);
			Require.NotNull(helper);
			this._context = context;
			this.IsOptional = isOptional;
			this.Helper = helper;
		}

		/// <summary>
		/// Writes the value of a class member to XML.
		/// </summary>
		/// <param name="targetObj">
		///		the object whose member value is to be written to XML
		/// </param>
		/// <param name="writer">where to write it</param>
		public abstract void WriteToXml (object targetObj, XmlWriter writer);

		/// <summary>
		/// Reads a value from XML into the class member for a given object.
		/// </summary>
		/// <param name="reader">where to read it from</param>
		/// <param name="targetObj">the object whose member value will be set</param>
		public abstract void ReadFromXml (NiceXmlReader reader, object targetObj);

		/// mainly for error reporting
		protected abstract MemberValueEncoding Encoding { get; }

		/// <summary> Dumps its guts.  For debugging only. </summary>
		public void Dump (System.IO.TextWriter tw, string indent = "")
		{
			tw.WriteLine(indent + "MemberMapping <" + this.XmlName + "> ");
			indent += "    ";
			tw.WriteLine(indent + "encoding=" + this.Encoding.ToString() + 
							", optional=" + this.IsOptional.ToString());
		}

		#region MemberHelper classes

		//=============================================================================
		/// <summary>
		/// Abstraction for getting or setting values for one field or property.
		/// </summary>

		protected abstract class MemberHelper
		{
		//	public readonly XerialTypeMapping ValueTypeMapping;

			/// <summary> constructor </summary>
			protected MemberHelper (XerialContext context)
			{
		//		this.ValueTypeMapping = context.TypeMappings.GetOrAdd(valueType);
			}

			/// <summary> Gets the value for this member from the given object. </summary>
			public abstract object GetMemberValue (object targetObj);

			/// <summary> Sets the value for this member in the given object. </summary>
			public abstract void SetMemberValue (object value, object targetObj);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Gets or sets values for one field.
		/// </summary>

		protected class FieldHelper : MemberHelper
		{
			private readonly FieldInfo FieldInfo;

			/// <summary> constructor </summary>
			public FieldHelper (XerialContext context, FieldInfo fi)
				: base(context)
			{
				Require.NotNull(fi);
				this.FieldInfo = fi;
			}
			
			/// <inherit/>
			public override object GetMemberValue(object targetObj)
			{
 				return this.FieldInfo.GetValue(targetObj);
			}

			/// <inherit/>
			public override void SetMemberValue(object value, object targetObj)
			{
 				this.FieldInfo.SetValue(targetObj, value);
			}

			/// <inherit/>
			public override string ToString ()
			{
				return "FieldHelper<" + this.FieldInfo.DeclaringType.Name + "." + this.FieldInfo.Name + ">";
			}
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Gets or sets values for one property.
		/// </summary>

		protected class PropertyHelper : MemberHelper
		{
			private readonly PropertyInfo PropertyInfo;
			private MethodInfo Getter;
			private MethodInfo Setter;

			/// <summary> constructor </summary>
			public PropertyHelper (XerialContext context, PropertyInfo pi)
				: base(context)
			{
				Require.NotNull(pi);
				this.PropertyInfo = pi;

				this.Getter = pi.GetGetMethod(true);
				this.Setter = pi.GetSetMethod(true);
			}

			/// <inherit/>
			public override object GetMemberValue (object targetObj)
			{
				return this.Getter.Invoke(targetObj, null);
			}

			/// <inherit/>
			public override void SetMemberValue (object value, object targetObj)
			{
				object[] valueObjArray = new object[1] { value };
				this.Setter.Invoke(targetObj, valueObjArray);
			}

			/// <inherit/>
			public override string ToString ()
			{
				return "PropertyHelper<" + this.PropertyInfo.DeclaringType.Name + "." + this.PropertyInfo.Name + ">";
			}
		}

		//-----------------------------------------------------------------------------

		#endregion
	}

	//#################################################################################
	/// <summary>
	/// Base class for mapping non-custom class members
	/// </summary>

	public abstract class StandardMemberMapping : XerialMemberMapping
	{
		/// <summary> The type mapping for this member. </summary>
		protected readonly XerialTypeMapping ValueTypeMapping;

		/// <summary> constructor  </summary>
		protected StandardMemberMapping (XerialContext context, string xmlName, Type valueType,
										MemberHelper helper, bool isOptional)
			: base(context, xmlName, helper, isOptional)
		{
			this.ValueTypeMapping = context.TypeMappings.GetOrAdd(valueType);
		}

	}

	//#################################################################################
	/// <summary>
	/// Maps one class member to an XML attribute
	/// </summary>

	public class AttributeMemberMapping : StandardMemberMapping
	{
		/// <inherit/>
		protected override MemberValueEncoding Encoding { get { return MemberValueEncoding.Attribute; } }

		/// <summary> Constructor for use with a field. </summary>
		public AttributeMemberMapping (XerialContext context, string attrName, FieldInfo fi, bool isOptional)
			: base(context, attrName, fi.FieldType, new FieldHelper(context, fi), isOptional)
		{
		}

		/// <summary> Constructor for use with a property. </summary>
		public AttributeMemberMapping (XerialContext context, string attrName, PropertyInfo pi, bool isOptional)
			: base(context, attrName, pi.PropertyType, new PropertyHelper(context, pi), isOptional)
		{
		}

		/// <inherit/>
		public override void WriteToXml (object targetObj, XmlWriter writer)
		{
			object memberValue = this.Helper.GetMemberValue(targetObj);
			if (memberValue == null)
				return;

			this.ValueTypeMapping.WriteToXmlAttr(this.XmlName, memberValue, writer);
		}

		/// <summary>
		/// Reads a value from an XML attribute into the class member for a given object.
		/// </summary>
		/// <param name="reader">
		///		Where to get the XML attribute.
		///		On entry: current node must be an Attribute.
		///		On exit: current node remains the same.
		/// </param>
		/// <param name="targetObj">the object whose member value will be set</param>
		public override void ReadFromXml (NiceXmlReader reader, object targetObj)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXml", reader);
			Require.Condition(reader.NodeType == XmlNodeType.Attribute);

			object memberValue = this.ValueTypeMapping.ReadFromXmlAttr(reader, this.XmlName);
			this.Helper.SetMemberValue(memberValue, targetObj);
		}
	}

	//#################################################################################
	/// <summary>
	/// Maps one class member to an XML (child) element or text.
	/// </summary>

	public abstract class ContentMemberMapping : StandardMemberMapping
	{
		/// <summary> Constructor for use with a field. </summary>
		protected ContentMemberMapping (XerialContext context, string elementName, 
														FieldInfo fi, bool isOptional)
			: base(context, elementName, fi.FieldType, new FieldHelper(context, fi), isOptional) { }

		/// <summary> Constructor for use with a property. </summary>
		protected ContentMemberMapping (XerialContext context, string elementName, 
													PropertyInfo pi, bool isOptional)
			: base(context, elementName, pi.PropertyType, new PropertyHelper(context, pi), isOptional) { }

		//-----------------------------------------------------------------------------
		/// <summary> Returns the mapping to use for writing a value to XML. </summary>
		/// <param name="targetObj">the object to be written</param>

		protected XerialTypeMapping GetMappingForWriting (object targetObj)
		{
			if (targetObj == null)
				return this.ValueTypeMapping;

			Type targetType = targetObj.GetType();
			Assert.Condition(this.ValueTypeMapping.TargetType.IsAssignableFrom(targetType));
			XerialTypeMapping tm = this.Context.TypeMappings[targetType];
			if (tm == null)
				throw new XerialException(this.XmlName + 
								" doesn't know how to xerialize type " + targetType.FullName + 
								" as " + this.Encoding.GetType().Name + 
								"." + this.Encoding.ToString());
			return tm;
		}

		//-----------------------------------------------------------------------------
		/// <summary> 
		/// Returns the mapping to use for reading a value from XML at the current
		/// position of the given reader.
		/// </summary>
	
		protected XerialTypeMapping GetMappingForReading (NiceXmlReader reader)
		{
			XerialTypeMapping tm = this.Context.TypeMappings[reader.Name];
			if (tm == null)
				throw new XerialException(reader, this.XmlName + " doesn't know how to dexerialize " +
								reader.NodeType.ToString() + " \"" + reader.Name + "\" as " + 
								this.Encoding.GetType().Name + "." + this.Encoding.ToString());
			return tm;
		}

	}

	//#################################################################################
	/// <summary>
	/// Maps one class member to an XML (child) element containing an inner element, thus:
	/// (TargetObj)(MemberName)(TypeName)value(/TypeName)(/MemberName)(/TargetObj)
	/// </summary>

	public class NestedElements_MemberMapping : ContentMemberMapping
	{
		/// <inherit/>
		protected override MemberValueEncoding Encoding { get { return MemberValueEncoding.NestedElements; } }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a field. </summary>

		public NestedElements_MemberMapping (XerialContext context, string elementName, 
														FieldInfo fi, bool isOptional)
			: base(context, elementName, fi, isOptional) { }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a property. </summary>

		public NestedElements_MemberMapping (XerialContext context, string elementName, 
													PropertyInfo pi, bool isOptional)
			: base(context, elementName, pi, isOptional) { }

		//-----------------------------------------------------------------------------

		/// <inherit/>
		public override void WriteToXml (object targetObj, XmlWriter writer)
		{
			object memberValue = this.Helper.GetMemberValue(targetObj);
			if (memberValue != null)
			{
				XerialTypeMapping tm = this.GetMappingForWriting(memberValue);
				writer.WriteStartElement(this.XmlName);
				tm.WriteToXmlElmnt(null, memberValue, writer);
				writer.WriteEndElement();
			}
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads a value from an XML element into the class member for a given object.
		/// </summary>
		/// <param name="reader">
		///		Where to get the XML element.
		///		On entry: current node must be an Element.
		///		On exit: current node will be the first node following the Element 
		///			node (for empty elements), or the first node following the
		///			EndElement node (for non-empty elements).
		/// </param>
		/// <param name="targetObj">the object whose member value will be set</param>

		public override void ReadFromXml (NiceXmlReader reader, object targetObj)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXml", reader);
			Require.Condition(reader.NodeType == XmlNodeType.Element);

			reader.MoveToContent();
			reader.ExpectElement(this.XmlName);

			if (!reader.IsEmptyElement)
			{
				reader.Read();	// skip over member wrapper element
				reader.MoveToContent();
				Assert.Condition(reader.NodeType == XmlNodeType.Element);

				XerialTypeMapping tm = this.GetMappingForReading(reader);
				object memberValue = tm.ReadFromXmlElmnt(reader);
				this.Helper.SetMemberValue(memberValue, targetObj);

				reader.MoveToContent();
				reader.ExpectEndElement(this.XmlName);
			}

			reader.Read();	// skip over empty element or end element
		}
	}

	//#################################################################################
	/// <summary>
	/// Maps one class member to an XML (child) element, using the XML element name
	/// for the member, and leaving the type implicit.
	/// </summary>

	public class ImplicitType_MemberMapping : ContentMemberMapping
	{
		/// <inherit/>
		protected override MemberValueEncoding Encoding { get { return MemberValueEncoding.ImplicitValueType; } }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a field. </summary>

		public ImplicitType_MemberMapping (XerialContext context, string elementName, 
														FieldInfo fi, bool isOptional)
			: base(context, elementName, fi, isOptional) { }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a property. </summary>

		public ImplicitType_MemberMapping (XerialContext context, string elementName, 
													PropertyInfo pi, bool isOptional)
			: base(context, elementName, pi, isOptional) { }

		//-----------------------------------------------------------------------------

		/// <inherit/>
		public override void WriteToXml (object targetObj, XmlWriter writer)
		{
			object memberValue = this.Helper.GetMemberValue(targetObj);
			if (memberValue != null)
				this.ValueTypeMapping.WriteToXmlElmnt(this.XmlName, memberValue, writer);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads a value from an XML element into the class member for a given object.
		/// </summary>
		/// <param name="reader">
		///		Where to get the XML element.
		///		On entry: current node must be an Element.
		///		On exit: current node will be the first node following the Element 
		///			node (for empty elements), or the first node following the
		///			EndElement node (for non-empty elements).
		/// </param>
		/// <param name="targetObj">the object whose member value will be set</param>


		public override void ReadFromXml (NiceXmlReader reader, object targetObj)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXml", reader);
			Require.Condition(reader.NodeType == XmlNodeType.Element);

			object memberValue = this.ValueTypeMapping.ReadFromXmlElmnt(reader);
			this.Helper.SetMemberValue(memberValue, targetObj);
		}
	}

	//#################################################################################
	/// <summary>
	/// Maps one class member to an XML (child) element, using the XML element name
	/// for the member type, and leaving the member name implicit.
	/// </summary>

	public class ImplicitName_MemberMapping : ContentMemberMapping
	{
		/// <inherit/>
		protected override MemberValueEncoding Encoding { get { return MemberValueEncoding.ImplicitMemberName; } }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a field. </summary>

		public ImplicitName_MemberMapping (XerialContext context, string elementName, 
														FieldInfo fi, bool isOptional)
			: base(context, elementName, fi, isOptional) { }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a property. </summary>

		public ImplicitName_MemberMapping (XerialContext context, string elementName, 
													PropertyInfo pi, bool isOptional)
			: base(context, elementName, pi, isOptional) { }

		//-----------------------------------------------------------------------------

		/// <inherit/>
		public override void WriteToXml (object targetObj, XmlWriter writer)
		{
			object memberValue = this.Helper.GetMemberValue(targetObj);
			if (memberValue != null)
			{
				XerialTypeMapping tm = this.GetMappingForWriting(memberValue);
				tm.WriteToXmlElmnt(tm.XmlName, memberValue, writer);
			}
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads a value from an XML element into the class member for a given object.
		/// </summary>
		/// <param name="reader">
		///		Where to get the XML element.
		///		On entry: current node must be an Element.
		///		On exit: current node will be the first node following the Element 
		///			node (for empty elements), or the first node following the
		///			EndElement node (for non-empty elements).
		/// </param>
		/// <param name="targetObj">the object whose member value will be set</param>


		public override void ReadFromXml (NiceXmlReader reader, object targetObj)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXml", reader);
			Require.Condition(reader.NodeType == XmlNodeType.Element);

			XerialTypeMapping tm = this.GetMappingForReading(reader);
			object memberValue = tm.ReadFromXmlElmnt(reader);
			this.Helper.SetMemberValue(memberValue, targetObj);
		}
	}

	//#################################################################################
	/// <summary>
	/// Maps one class member to the text (content) of the parent element, thus:
	/// (TargetObj)value(/TargetObj)
	/// </summary>

	public class TextMemberMapping : ContentMemberMapping
	{
		/// <inherit/>
		protected override MemberValueEncoding Encoding { get { return MemberValueEncoding.Text; } }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a field. </summary>

		public TextMemberMapping (XerialContext context, string elementName,
														FieldInfo fi, bool isOptional)
			: base(context, elementName, fi, isOptional) { }

		//-----------------------------------------------------------------------------
		/// <summary> Constructor for use with a property. </summary>

		public TextMemberMapping (XerialContext context, string elementName,
													PropertyInfo pi, bool isOptional)
			: base(context, elementName, pi, isOptional) { }

		//-----------------------------------------------------------------------------

		/// <inherit/>
		public override void WriteToXml (object targetObj, XmlWriter writer)
		{
			object memberValue = this.Helper.GetMemberValue(targetObj);
			if (memberValue != null)
			{
				XerialTypeMapping tm = this.GetMappingForWriting(memberValue);
				tm.WriteToXmlText(memberValue, writer);
			}
		}

		//-----------------------------------------------------------------------------

		/// <inherit/>
		public override void ReadFromXml (NiceXmlReader reader, object targetObj)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXml", reader);
		//	Require.Condition(reader.NodeType == XmlNodeType.Element);

			reader.MoveToContent();
		//	reader.ExpectElement(this.XmlName);

			if (!reader.IsEmptyElement)
			{
				object memberValue = this.ValueTypeMapping.ReadFromXmlText(reader);
				this.Helper.SetMemberValue(memberValue, targetObj);
				reader.Skip();
			}

		//	reader.Read();	// skip over empty element or end element
		}

		//-----------------------------------------------------------------------------
	}

	//#################################################################################
	/// <summary>
	/// Maps one class member to XML via custom methods provided by the user.
	/// </summary>
	/// <remarks>
	/// This is used for both attribute members and element members.
	/// </remarks>

	public class CustomMemberMapping : XerialMemberMapping
	{
		private readonly Type ValueType;
		private readonly MethodInfo Reader;
		private readonly MethodInfo Writer;

		private static readonly Type[] ReaderParamTypes = 
				{ typeof(NiceXmlReader), typeof(string), typeof(Type), typeof(object) };
		private static readonly Type[] WriterParamTypes = 
				{ typeof(object), typeof(object), typeof(string), typeof(XmlWriter) };

		/// <inherit/>
		protected override MemberValueEncoding Encoding { get { return this._encoding; } }
		private readonly MemberValueEncoding _encoding;

		//-----------------------------------------------------------------------------

		/// <summary>
		/// Constructs a member mapping for a field.
		/// </summary>
		public CustomMemberMapping (XerialContext context, string xmlName, FieldInfo fi,
									MemberValueEncoding encoding, bool isOptional, 
									string readerName, string writerName)
			: base(context, xmlName, new FieldHelper(context, fi), isOptional)
		{
			Require.NotNull(readerName);
			Require.NotNull(writerName);
			this.ValueType = fi.FieldType;
			this._encoding = encoding;
			this.Reader = ResolveMethodName(fi.DeclaringType, readerName, ReaderParamTypes);
			this.Writer = ResolveMethodName(fi.DeclaringType, writerName, WriterParamTypes);
		}

		/// <summary>
		/// Constructs a member mapping for a property.
		/// </summary>
		public CustomMemberMapping (XerialContext context, string xmlName, PropertyInfo pi,
									MemberValueEncoding encoding, bool isOptional, 
									string readerName, string writerName)
			: base(context, xmlName, new PropertyHelper(context, pi), isOptional)
		{
			Require.NotNull(readerName);
			Require.NotNull(writerName);
			this._encoding = encoding;
			this.ValueType = pi.PropertyType;
			this.Reader = ResolveMethodName(pi.DeclaringType, readerName, ReaderParamTypes);
			this.Writer = ResolveMethodName(pi.DeclaringType, writerName, WriterParamTypes);
		}

		//-----------------------------------------------------------------------------

		private static MethodInfo ResolveMethodName (Type parentType, string name, Type[] paramTypes)
		{
			MethodInfo mi = parentType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | 
		                           BindingFlags.Static | BindingFlags.Instance, null, paramTypes, null);
			if (mi == null)
			{
				string paramTypeNames = "";
				foreach (Type pt in paramTypes)
					paramTypeNames += ", " + pt.Name;
				throw new XerialException(parentType.FullName + " doesn't have a method named \"" +
		                    name + "\" with parameter types (" + paramTypeNames.Substring(2) + ")");
			}
			else return mi;
		}

		//-----------------------------------------------------------------------------

		//private static T ResolveMethodName<T> (Type parentType, string name, Type[] paramTypes) where T : class
		//{
		//    MethodInfo mi = parentType.GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | 
		//                           BindingFlags.Static | BindingFlags.Instance, null, paramTypes, null);
		//    if (mi == null)
		//    {
		//        string paramTypeNames = "";
		//        foreach (Type pt in paramTypes)
		//            paramTypeNames += ", " + pt.Name;
		//        throw new XerialException(parentType.FullName + " doesn't have a method named \"" +
		//                    name + "\" with parameter types (" + paramTypeNames.Substring(2) + ")");
		//    }
		//    else
		//        return Delegate.CreateDelegate(typeof(T), mi) as T;
		//    //	obscure lore: the line below won't compile.
		//    //	return (T) Delegate.CreateDelegate(typeof(T), mi);
		//}

		//-----------------------------------------------------------------------------

		/// <inherit/>
		public override void WriteToXml (object targetObj, XmlWriter writer)
		{
			object memberValue = this.Helper.GetMemberValue(targetObj);
			this.Writer.Invoke(targetObj, new object[] {targetObj, memberValue, this.XmlName, writer});
		}

		/// <inherit/>
		public override void ReadFromXml (NiceXmlReader reader, object targetObj)
		{
			if (DebugLogEnabled) this.LogCall("ReadFromXml", reader);
			Require.Condition(reader.NodeType == XmlNodeType.Attribute);

			this.Reader.Invoke(targetObj, new object[] {reader, this.XmlName, this.ValueType, targetObj});
		}
	}

	/// <summary>
	/// Custom method for reading a member value from XML into the member's parent object.
	/// </summary>
	/// <param name="reader">where to read from</param>
	/// <param name="xmlName">the XML name of the attribute or child element containing the member value</param>
	/// <param name="memberType">the type of the member</param>
	/// <param name="parentObject">the object in which the member value will be stored</param>
	public delegate void MemberReader(NiceXmlReader reader, string xmlName, Type memberType, object parentObject);

	/// <summary>
	/// Custom method for writing a member value to XML.
	/// </summary>
	/// <param name="parentObject">the object whose member value is to be written</param>
	/// <param name="memberValue">the member value to write (may be null)</param>
	/// <param name="xmlName">the XML name of the attribute or child element that will contain the member value</param>
	/// <param name="writer">where to write it</param>
	public delegate void MemberWriter(object parentObject, object memberValue, string xmlName, XmlWriter writer);

	//#################################################################################
}
